import pandas as pd
import numpy as np
import logging 
from itertools import combinations

import random
from sklearn.preprocessing import LabelEncoder

from utils import print_warning, print_function, cal_info_gain_rate

class FeatureDetector():
    '''
    Detect new features by current features.
    
    Main methods:
        1. detect_catergory_cross: Detect two category cross feature
        2. detect_continuous_split: Detect continuous feature best spliting points
        
    Parameters
    ----------
    features : DataFrame, shape(n_samples, n_features)
        The feature data.
    labels : array-like, shpe(n_samples, )
        The lable of crresponding sample.
    '''
    
    def __init__(self, features, labels=None):
        self.features = features
        self.labels = np.asarray(labels)
        
    def detect_catergory_cross(self, cat_features, degree=2, filename=None, top_k=5):
        '''
        Detect category cross feature, return top k cross features with higher info gain rate.
        
        Parameters
        ----------
        cat_features : array-like
            The features name to be detected and crossed.
        degree : int
            The crossing dimension mean that how many feature will be crossed.
        filename : str
            The path where the info gain rate of crossed feature will be saved. The info gain rate is like 
            as follows:
            |=============================|
            |feature              |  igr  |
            |feature1_feature2    |  rate1|
            |feature1_feature3    |  rate2|
            |=============================|
        top_k : int
            The number of crossed features to be returned.
            
        Returns
        -------
        detected_features : list of str
            The list of the crossed feature names. If you want to get the corresponding data, you can get 
            it like self.features[detected_features].
        detected_igrs : list of float
            The list of corresponding info gain rate of detected_features.
        '''
        for feature in cat_features:
            if feature not in self.features.columns:
                raise ValueError("The \"%s\" feature is not in data!" % feature)
        if int(degree) < 2:
            raise ValueError("The degree parameter value should be greater than 1!")
            

        if degree > len(cat_features):
            print_warning("Invalid degree value %d, the length of cat_features is %d!" % (degree, len(cat_features)))
        pairs = []
        le = LabelEncoder()
        for e in range(2, degree + 1):
            for feat_tuple in combinations(cat_features, e):
                new_feature_name = '_'.join(feat_tuple)
                if new_feature_name not in self.features.columns:
                    self.features[new_feature_name] = le.fit_transform(self.features.apply(lambda x: '_'.join([str(e) for e in x[list(feat_tuple)].values]), axis=1))
                igr = cal_info_gain_rate(self.features[new_feature_name].values, self.labels)
                pairs.append((new_feature_name, igr))
        sorted_pairs = sorted(pairs, key=lambda x: x[1], reverse=True)
        detected_pairs = sorted_pairs[0:top_k]
        detected_features = [x[0] for x in detected_pairs]
        detected_igrs = [x[1] for x in detected_pairs]
        
        tmp_df = pd.DataFrame({'feature': [x[0] for x in pairs], 'info_gain_rate': [x[1] for x in pairs]})
        if filename is not None:
            tmp_df.to_csv(filename, index=False)
        return detected_features, detected_igrs
    
    def _get_ranges(self, sorted_feature, y, min_split_num):
        '''
        Get the split points by recursive split the range until the number of the range is 
        less than min_split_num.
        '''
        if len(sorted_feature) < min_split_num:
            return []
        max_igr = -1.0
        max_idx = 0
        length = len(y)
        for i in range(min_split_num, length - min_split_num - 1):
            tmp_feature = [0 if idx <= i else 1 for idx in range(0, length)]
            igr = cal_info_gain_rate(tmp_feature, y)
            if igr > max_igr:
                max_igr = igr
                max_idx = i 
        if max_igr <= 0: 
            return []
        left_split = self._get_ranges(sorted_feature[0 : max_idx], y[0 : max_idx], min_split_num)
        right_split = self._get_ranges(sorted_feature[max_idx + 1 : ], y[max_idx + 1 : ], min_split_num)
        return left_split + [sorted_feature[max_idx]] + right_split

    def _range_mapping(self, x, ranges):
        '''
        Mapping the continuous feature to the int value by the ranges infomation.
        '''
        idx=0
        for idx, bound in enumerate(ranges):
            if x <= bound:
                return idx
        return idx + 1
            
    def detect_continuous_split(self, continuous_features, min_split_num=10):
        '''
        Detect the split point of the continuous feature.
        
        Parameters
        ----------
        continuous_features : list of str
            The list of continuous feature names to be detected.
        min_split_num : int
            The leaest number of range allowed to be splited. Tf the number of the range is less than min_split_num,
            the range will not be splited.
            
        Returns
        -------
        new_features : list of str
            The new detected discreted feature names corresponding to the continuous features. The new feature name is 
            added the '_discret' suffix.
        '''
        for feature in continuous_features:
            if feature not in self.features.columns:
                raise ValueError("The \"%s\" feature is not in data!" % feature)
        
        new_feature_suffix = '_discret'
        new_features = []
        for col in continuous_features:
            pairs = [(feature, label) for feature, label in zip(self.features[col].values, self.labels)]
            sorted_pairs = sorted(pairs, key=lambda x: x[0])
            ranges = self._get_ranges([x[0] for x in sorted_pairs], [x[1] for x in sorted_pairs], min_split_num)
            print("%s: %s" % (col, str(ranges)))
            new_feature = col + new_feature_suffix
            self.features[new_feature] = self.features[col].map(lambda x: self._range_mapping(x, ranges))
            new_features.append(new_feature)
        return new_features
        
